<?php
/**
 * Ogg thumbnailer class.
 * Try to create a thumbnail from a movie
 * - if "oggThumb" is found in path use oggThumb
 * - if ffmpeg is installed, us ffmpeg
 * - return a blank image
 *
 * @author Matthijs van Henten, Lodewijk Loos <matthijs@waag.org>, <lodewijk@waag.org>
 * @license Gnu GPL 2.0
 */
class Ogg_Thumb{
	// define search path; Add your own if needed
	const OGGTHUMB_SEARCH_PATH = '/opt/local/bin;/opt/bin;/usr/local/bin;/usr/bin';

	private $log;
	private $movie;
	private $resource;
	private $timeCode;

	/**
	 * Class constructor. You may supply any properties beforehand as $key => $value pairs
	 * The __set function is called to parse the arguments
	 *
	 * @return
	 */
	public function __construct( array $args = array() ){
		foreach( $args as $key => $value ){
			if( null !== $value && ! empty( $value ) ){
				$this->__set( $key, $value );
			}
		}
	}

	/**
	 * Magic setter. Looks for a setter-function and calls that function
	 *
	 * @param string $name Name of the property to be set
	 * @param mixed $value Value for the property
	 *
	 * @return mixed Whatever the setter returns
	 */
	public function __set( $name, $value ){
		if( ( $method = 'set' . ucfirst( $name ) ) && method_exists( $this, $method ) ){
			$value = (array) $value;
			return call_user_func_array( array( $this, $method ), $value );
		}
	}

	/**
	 * Magic getter. Returns the property $name by calling a getter method, or NULL
	 *
	 * @param string $name Name of the property
	 * @return mixed $property or null
	 */
	public function __get( $name ){
		if( ( $method = 'get' . ucfirst( $name ) ) && method_exists( $this, $method ) ){
			return $this->$method();
		}
	}

	/**
	 * clean up the resource
	 */
	public function __destroy(){
		imagedestroy( $this->getResource() );
	}

	/**
	 * Set movie path. By setting the movie path, current resource is reset
	 * and new thumbnail will be generated.
	 *
	 * @param string $path The movie path
	 * @return At_OggThumb $this Fluent interface
	 */
	public function setMovie( $path ){
		$this->resource = null;
		$this->movie    = (string) $path;
		return $this;
	}

	/**
	 * Set the timecode to generate thumbnails from. Current resource is restet
	 * and new thumbnails will be generated
	 *
	 * @param int $timeCode Timecode in seconds
	 * @return At_OggThumb $this Fluent interface
	 */
	public function setTimeCode( $timeCode ){
		$this->resource = null;
		$this->timeCode = (int) $timeCode;

		return $this;
	}

	/**
	 * Return the with of the current resource
	 */
	public function getWidth(){
		return imagesx( $this->getResource() );
	}

	/**
	 * Return the with of the current resource
	 */
	public function getHeight(){
		return imagesy( $this->getResource() );
	}

	/**
	 * Create a new thumbnail of $with and $height using the current resource
	 *
	 * @param int $width With of the thubmanil
	 * @param int $height Height of the thumbnail
	 *
	 * @return string $imageData Imagedata ( jpeg ) as string
	 */
	public function getThumbnail( $width = null, $height = null){
		if( null == $width ){
			$width = $this->getWidth();
		}
		if( null == $height ){
			$height = $this->getHeight();
		}

		$image = $this->resize( $width, $height );

		ob_start();
		imagejpeg( $image, null, 83 );
		$data = ob_get_clean();

		imagedestroy( $image );
		return $data;
	}

	/**
	 * Return the command output from exec if it exists
	 *
	 * @return mixed $log or NULL
	 */
	public function getLog(){
		if( null !== $this->log && is_array( $this->log ) ){
			return join( "\n", $this->log );
		}
		return '';
	}

	/**
	 * Do an imagecopyresampled on current resource
	 *
	 * @param int $width Width of the desired image
	 * @param int $height Height for the desired image
	 *
	 * @return resource $gd Gd resource
	 */
	public function resize( $width, $height ){
		$gd = $this->getResource();

		$new = imagecreatetruecolor( $width, $height );

		imagecopyresampled( $new, $gd, 0, 0, 0, 0, $width, $height, imagesx( $gd ), imagesy( $gd ) );
		return $new;
	}

	/**
	 * Retrieve the GD resource and create a new resource from thumbnail
	 *
	 * @return resource $gd Gd resource
	 */
	private function getResource(){
		if( null == $this->resource ){
			$this->setResource( $this->createThumbnail( $this->getTimeCode() ) );
		}
		return $this->resource;
	}

	/**
	 * Set resource. Checks if a valid resource is set
	 *
	 * @return void
	 */
	private function setResource( $gd ){
		if( ! is_resource( $gd ) ){
			throw new Exception( 'This is not a valid resource!' );
		}
		$this->resource = $gd;
	}

	/**
	 * Return timecode. if timecode is not set, set timecode to 1
	 *
	 * @return int $timeCode
	 */
	private function getTimeCode(){
		if( null == $this->timeCode ){
			$this->setTimeCode( 1 );
		}
		return $this->timeCode;
	}

	/**
	 * Get movie path. Should this assert that the movie exists?
	 *
	 * @return string $movie
	 */
	private function getMovie(){
		if( file_exists( $this->movie ) ){
			return $this->movie;
		}
	}

	/**
	 * Create an $gd imageresource preferably from an actual movie
	 * falls back to creating a dummy image
	 *
	 * @param int $timeCode Timecode in seconds
	 * @return resource $gd
	 */
	private function createThumbnail( $timeCode ){
		$file = $this->getMovie();

		if( null !== $file && !empty( $file ) ){
			if( null !== ( $cmd = $this->getPath( 'oggThumb' ) ) ){
				return $this->thumbnailOggThumb( $cmd, $file, $timeCode );
			}
			else if( null !== ( $cmd = $this->getPath('ffmpeg') )
					&& null !== $this->getPath( 'oggz-chop' ) ){
				return $this->thumbnailFFmpeg( $cmd, $file, $timeCode );
			}
		}
		return $this->getDummyFrame();
	}

	/**
	 * Create a dummy frame
	 * @return resource $resource
	 */
	private function getDummyFrame(){
		$gd = imagecreatetruecolor( 400, 300 );

		imagefill( $gd, 0, 0, imagecolorallocate( $gd, 255,255,255) );
		$color = imagecolorallocate( $gd, 96,96,96);

		// border and cross
		imageline( $gd, 0, 0, 400, 0, $color );
		imageline( $gd, 399, 0, 399, 300, $color );
		imageline( $gd, 400, 299, 0, 299, $color );
		imageline( $gd, 0, 300, 0, 0, $color );
		imageline( $gd, 0, 0, 400, 300, $color );
		imageline( $gd, 0, 300, 400, 0, $color );

		return $gd;
	}

	/**
	 * Create a thubmanil using OggThumb
	 *
	 * @param string $cmd Path to oggThumb
	 * @param string $file Path to movie file
	 * @param int $timeCode Timecode in seconds
	 *
	 * @return resource $gd GD resource
	 */
	private function thumbnailOggThumb( $cmd, $file, $timeCode ){
	        set_time_limit(0);
	        $output   = array();

		$cwd = getcwd();


		// oggthumb does not allow us to set a filename
		// the workaround is to create a temporary dir, and use that
		// dir to output the filename in
		$tmpdir = sys_get_temp_dir()  . basename($file) . '_' . $timeCode;

		@mkdir( $tmpdir, 0700, true );

		chdir($tmpdir);


		$exec = sprintf('%s -s 640x360 -t%d %s',
			$cmd,
			$timeCode,
			$file
		);

		exec( $exec, $output );

		array_unshift( $output, $exec );
		array_unshift( $output, 'path: ' . $tmpdir );


		$filename = basename( $file );
		$filename = explode( '.', $filename );
		array_pop( $filename );

		$filename = join( '.', $filename ); // possiby files with '.' in the name
		$path = sprintf( '%s/%s_0.%s', $tmpdir, $filename, 'jpg' );

		array_unshift( $output, $path );

		$this->log = $output;


		if( file_exists( $path ) ){
			$gd = imagecreatefromstring( @file_get_contents( $path ) );
			chdir($cwd);
			@unlink( $path );
			@rmdir( $tmpdir );
		}
		else{
			$gd = $this->getDummyFrame();
		}

		return $gd;
	}

	/**
	 * Tries to create a thumbnail using FFMPEG and OGGZCHOP
	 * This actually assumes you have oggz-chop installed somewhere!
	 *
	 * @param string $cmd Path to ffmpeg
	 * @param string $file Path to movie file
	 * @param int $timeCode Timecode in seconds
	 *
	 * @return resource $gd GD resource
	 */
	private function thumbnailFFmpeg( $cmd, $file, $timeCode ){
        set_time_limit( 3600 );
		$output = array();
		$path   = sprintf( '/tmp/%s_%s_%s.jpg', basename($this->getMovie()), $timeCode, uniqid() );

        exec( sprintf('%s -s %d -e %d %s | %s -intra -i - -vframes 1 %s',
			$this->getPath( 'oggz-chop' ),
			$timeCode,
			$timeCode + 5,
			$file,
			$cmd,
			$path
		), $output );

		// save commandline output
		$this->log = $output;

		if( file_exists( $path ) ){
			// return a gd resource
			$gd = imagecreatefromstring( @file_get_contents( $path ) );
			@unlink( $path );
		}
		else{
			$gd = $this->getDummyFrame();
		}

		return $gd;
	}

	/**
	 * Check for the existance of $name executable and returns the path
	 * or NULL
	 *
	 * @param $string $name Name of the executable
	 * @param return $path or null The path if found of the executable
	 */
	private function getPath( $name ){
		$search = self::OGGTHUMB_SEARCH_PATH;

		// allow override from outside?
		if( defined( 'OGGTHUMB_SEARCH_PATH' ) ){
			$search = OGGTHUMB_SEARCH_PATH;
		}

		foreach( explode( ';', $search ) as $path ){
			$path = sprintf( '%s/%s', rtrim( $path, '/' ), $name );

			if( file_exists( $path ) && is_executable( $path ) ){
				return $path;
			}
		}
	}
}
